kubernetes StatefulSet
介绍
我们之前学习过ReplicaController(RC),ReplicaSet(RS),Deployment(deploy)等,这些资源均可以通过一个Pod模板创建多个Pod副本.这些pod除了IP和名字不一样外,其他均一模一样.
如果pod模板关联到特定的持久卷声明,那么这些pod都共享同一个存储.这些pod是完全一样,也无所谓运行在哪个节点上.可以任何删除和替换.
但是在实际场景中,并不是所有的应用都满足这样的要求.比如主从关系,主备关系,数据存储类应用等.这些场景每个pod都会在本地保留一份数据,而且与其他Pod有数据对应关系.如果pod一旦被删除,即便新创建个pod出来,实例之间的对应关系也会失败,从而导致应用失败.
为了支持有状态的应用,Kubernetes在Deployment的基础上扩展出了StatefulSet资源
StatefulSet 特点
statefulSet的特点即是有状态的应用特点:
稳定且需要唯一的网络标识符;
- 这要求pod的主机名和IP地址永久不变,即使删除了一个pod,新创建的Pod也必须继承前一个Pod的主机标识符
稳定且持久的存储;
- 可实现持久存储,新增或者减少pod,存储不会随之变化,并且删除一个pod时,关联到此pod的存储不会随之删除
要求有序,平滑的部署和扩展
- 在Mysql等集群,要先启动主节点,然后启动从节点,第二从节点等等
要求有序,平滑的终止和删除
- 同样,应用的终止和删除也是有顺序的,按照启动的逆序进行.例如Mysql启动时,先启动主节点,再启动从节点.终止的时候,先关闭从节点,再关闭主节点
有序的滚动更新
- 在Mysql更新时,也应该先更新所有从节点.最后更新主节点
StatefulSet 依赖组件
结合以上的特点,StatefulSet依赖以下组件:
- headless service(无头服务): 用于DNS发现各pod网络地址
- pv存储卷: 底层存储卷
- volumeClaimTemplate(PVC申请模板): 用于每个Pod申请独立的存储卷
headless Service
我们以前学习过,Service是Kubernetes项目中用来将一组Pod暴露给外界访问的一种机制.外部客户端通过Service地址可以随机访问到某个具体的Pod
之前学过几种Service类型,包括nodeport,loadbalancer等等.所有这类Service都有一个VIP(虚拟IP),访问Service VIP,Service会将请求转发到后端的Pod上,
还有一种Service是Headless Service(无头服务),这类Service自身不需要VIP,当DNS解析该Service时,会解析出Service后端的Pod地址.这样设置的好处是Kubernetes项目为Pod分配唯一的”可解析身份”,只要知道一个pod的名字和对应的Headless Service名字,就可以通过这条DNS访问到后端的Pod
持久存储
我们知道通过headless Service使Pod有一个稳定的网络标识,那么存储呢?有状态的应用必须有自己独立的存储,即便这个pod被删除,新创建出来的pod(新pod与旧pod拥有相同的网络表示)也必须挂载相同的存储.
之前在学习kubernetes的存储时,我们学习过PV,PVC存储卷,通过pod模板关联一个持久卷声明就可以为pod提供一个持久卷存储.因为持久卷声明(PVC)和持久卷(PV)是一对一关系.但是之前接触过的ReplicationController,ReplicaSet,Deployment等资源创建的pod是同一个模板创建的,所以共享的是同一个持久卷存储.而StatefulSet要求每个pod都需要有独立的持久卷声明和存储.所以StatefulSet要求关联到一个或多个不同的持久卷声明模板.这些持久卷声明会在pod创建之前准备就绪,并且关联到每个pod中.
持久卷的创建和删除
扩容一个StatefulSet副本时,会创建2个或者多个对象: pod实例已经与之关联的一个或者多个持久卷声明.但是当StatefulSet缩容时,只会删除一个Pod,而留下持久卷声明.这就意味着删除Pod时,与pod关联的持久卷存储数据并不会被删除.如果持久卷声明被手动删除,那么持久卷上的数据则会消失.
因为缩容会保留持久卷声明,所以在随后的扩容操作中,新的pod实例会使用绑定在持久卷上相同的声明和其上的数据.所以如果因为误操作而缩容一个StatefulSet副本后,可以做一次扩容操作,新的pod实例会运行到与之前完全一致的状态,甚至连pod名字也是一样的
部署StatefulSet应用
部署StatefulSet应用之前,需要创建几个不同类型的对象.
一个演示用的docker镜像
存储数据文件的持久卷(PV)
一个Headless Service服务实例
Statefulset模板
准备一个docker镜像
这里使用书上提供的luksa/kubia-pet镜像,这个镜像是一个Node应用,当应用接收到一个POST请求时,将请求中的body写入到某个文件,当接收到一个GET请求时,返回pod主机名以及改文件中的内容.
创建持久化存储卷(pv)
因为稍后会调度StatefulSet创建3个副本.所以这里需要3个持久卷.如果计划调度更多的副本,则需要创建更多的持久卷..
之前在学习存储知识的时候介绍过存储卷,所以具体不演示,以下是创建3个PV持久卷的配置文件
1 | [root@k8s-master ~]# cat statefulset-kubia-pv.yaml |
以前接触过在yaml文件中添加—3个横杠使的在一个文件中可以区分定义多个资源,这次定义一个List对象,然后把各个资源作为List对象的各个项目.这2种方法均可以在一个YAML文件中定义多个资源
现在已经定义了个3个底层的PV持久卷
1 | [root@k8s-master ~]# kubectl get pv |
创建Headless Service
下面是headless service的配置文件,唯一需要注意的是该类型服务的clusterIP属性必须为None
1 | apiVersion: v1 |
创建服务
1 | [root@k8s-master ~]# kubectl get svc |
创建Statefuleset
statefulset资源的配置和RS,deployment等没有太大的区别,这里使用了一个新的组件volumeClaimTemplates.其中定义了一个持久卷声明.该组件会为每个Pod创建一个独立的持久卷声明.
这个组件是在statefulset资源的spec全局对象下,虽然在pod的template模板中并没有创建持久卷声明(而是直接通过volumeMounts属性来挂在).但是Statefulset在创建时,会自动将volumeClaimTemplate定义的持久卷声明关联到pod中.
1 | [root@k8s-master ~]# cat statefulset-kubia.yaml |
注意:volumeClaimTemplates组件一定要声明存储类型storageClassName,如果没有声明这一点则Pod一直处于Pending状态.并且会有以下报错信息
1 | [root@k8s-master ~]# kubectl describe po statefulset-kubia-v1-0 |
查看PVC提示没有找到PV
1 | [root@k8s-master ~]# kubectl describe pvc data-statefulset-kubia-v1-0 |
创建statefulset资源,列出pod资源.和rs,rc,deployment不同的是,他们会一次性创建完所有的pod,而statefulset会在每一个pod完全就绪后,才会创建第二个.
statefulset这样做是因为:状态明确的集群应用对同事有2个集群成员启动引起的竞争情况是非常敏感的.所以依次启动每个成员是比较安全可靠的.
1 | [root@k8s-master ~]# kubectl get pods |
现在3个Pod副本都已经被创建完成.
1 | [root@k8s-master ~]# kubectl get pods |
statefulset自动创建了3个PVC,并且各自与3个pv自动关联
1 | [root@k8s-master ~]# kubectl get pvc |
和RS,RC,Deployment等资源不同的是,Statefulset部署的pod名称并非是随机的,而是pod模板名加上一个序号,这个序号从0开始,依次增加.
PVC的名称格式是PVC的名字+pod名.每个pod自动创建一个PVC,并且该PVC自动关联到一个后端的PV持久卷
访问POD
由于创建的Service类型是Headless service模式,所以不能通过它来访问pod,而是需要直接连接到每个后端单独的pod.(或者是创建一个普通的Service,但是这样也不允许访问指定的pod)
这次介绍如何通过API服务器与pod通信.API服务器可以通过代理直接连接到指定的pod.可以通过如下的URL
1 | <apiServerHost>:<port>/api/v1/namespaces/default/pods/pods名称/proxy/<path> |
在k8s的master节点运行下面命令,下面命令运行一个kubectl proxy.从而可以让proxy去API服务器通信,而不必使用麻烦的授权和SSL证书来直接与API服务器通信
1 | kubectl proxy |
现在就可以直接访问Pod了.开启另一个master服务器终端.通过curl访问某个Pod.比如访问statefulset-kubia-v1-0这个Pod容器
1 | [root@k8s-master ~]# curl localhost:8001/api/v1/namespaces/default/pods/statefulset-kubia-v1-0/proxy/ |
这种访问方式经过了2层的中间代理:
1.curl命令发送给kubectl proxy
2. kubectl proxy 带上认证TOKEN转发给API服务器
3. API服务器再通过pod容器的实际IP地址将请求转发到后端的Pod
下面是发送一个post请求到statefulset-kubia-v1-0的例子
1 | [root@k8s-master ~]# curl -X POST -d "Hey There ! This greeting was submitted to statefulset-kubia-v1-0" \ |
当我们访问其他的pod容器时,并没有返回写入的数据,这和期望的一致,说明每个节点都有各自独立的存储状态
1 | [root@k8s-master ~]# curl localhost:8001/api/v1/namespaces/default/pods/statefulset-kubia-v1-1/proxy/ |
删除pod,重新调度
之前我们在statefulset-kubia-v1-0这个pod节点写入了一条数据,这次我们删除这个Pod,等它被重新调度,然后检查它是否还会返回与之前一致的数据
1 | [root@k8s-master ~]# kubectl delete po statefulset-kubia-v1-0 |
删除一个Pod,当Pod重新被调度时不一定是原节点,有可能会调度到另外一个节点
从上面的实验中可以得出2个结论:
- statefulset的pod被重新调度时,会新创建一个和之前一模一样的Pod(包括主机名称,pod名,存储)
- 当pod被删除,重新调度后持久化数据与之前一模一样.
statefulSet滚动更新
1.7版本之前默认的On Delete更新策略
statefulset在1.7版本开始支持滚动更新..在1.7版本之前默认的更新测量是On Delete
.这种侧列和ReplicaSet类似.当更新了配置文件后,旧的pod并不会被自动删除,而是需要手动删除.
下面这个例子中,将镜像更换为luksa/kubia-pet-peers.副本数从3个增加到4个.(为此,我们需要提前再创建一个pv-4
1 | 编辑pv配置文件,增加pv-4(前提是nfs服务器上实现存在/data/k8s/pv-4目录 |
更新statefulset配置文件
1 | #更新配置文件 |
通过Pod的存活字段可以看到之前旧版本的Pod并没有被自动删除,而是新增了一个副本.这和ReplicaSet的机制类似.
自动滚动更新策略
编辑statefulset配置文件,将镜像版本改回到luksa/kubia-pet
1 | spec: |
应用新的配置文件,此时会触发自动更新
1 | [root@k8s-master ~]# kubectl apply -f statefulset-kubia.yaml |
发现了什么? 当滚动更新时,kubectl会以倒序的方式,从最末尾一个pod开始依次更新.
StatefulSet的滚动更新策略不同于Deployment可以指定maxSuge参数指定一次同时更新的pod数量,而是只能单个方式进行依次更新
StatefulSet还支持partition(分区)的更新策略,具体可以查看官网
无论是何种更新策略.Pod的数据(包括主机名,存储)都会持久化.再次访问第0个pod,存储数据依然存在
1 | [root@k8s-master ~]# curl localhost:8001/api/v1/namespaces/default/pods/statefulset-kubia-v1-0/proxy/ |
StatefulSet 如何处理节点失效
在node2上关闭网卡来模拟这台服务器掉线,观察statefulSet处理节点失效的情况
注意关闭节点网卡前请确保可以通过控制台连接服务器,因为这意味着无法ssh远程登录
node2节点已经关闭,状态为notready
1 | [root@k8s-master ~]# kubectl get node |
过一段时间后,所有node2节点上的Pod为Terminating终止状态
1 | [root@k8s-master ~]# kubectl get pods -o wide |
删除不健康的Pod
当尝试手动删除pod时,发现永远都无法删除
1 | [root@k8s-master ~]# kubectl delete po statefulset-kubia-v1-0 |
在另一个终端上查看该pod.发现虽然pod被Terminating挂起,但是容器仍然处于运行状态
1 | [root@k8s-master ~]# kubectl describe pods statefulset-kubia-v1-0 |
强制删除
带上参数--force --grace-period 0
可以强制删除一个pod
1 | [root@k8s-master ~]# kubectl delete po statefulset-kubia-v1-0 --force --grace-period 0 |
此时第0个pod已经重新创建,并且运行到node1节点,而另一个pod依然处于Terminating状态
node2节点恢复正常
当节点恢复正常后,很快node和pod都全部恢复正常.此时第0个pod依然还是挂在在node1节点.第1个pod的状态迅速从Terminating状态变为Running状态
1 | [root@k8s-master ~]# kubectl get pods -o wide |
本章总结
Stateful和RS,deployment的用法总体没有太大区别,下面是这2种资源的对比
特性 | Deployment | StatefulSet |
---|---|---|
是否暴露到外网 | 可以 | 一般不 |
请求面向的对象 | ServiceName | 指定pod的域名 |
灵活性 | 通过Service(名称或者IP)访问后端Pod | 可以访问任意一个pod |
易用性 | 只需要关心Service信息即可 | 需要知道访问pod的名称,Headless Service名称 |
PV/PVC稳定性 | 无法保障绑定关系 | 可以保障 |
pod名称稳定性 | 使用一个随机的名称后缀,重启后会随机生成另外一个.名称不重复 | 稳定,每次都一样 |
升级更新顺序 | 随机启动.如果pod宕机重启,也是随机分配一个Node节点重新启动 | pod按顺序依次启动,如果pod宕机,依然使用相同的Node节点和名称 |
停止顺序 | 随机停止 | 倒序停止 |
集群内部服务发现 | 只能通过Service访问随机的一个Pod | 可以打通pod之间的通信 |
性能开销 | 无需维护pod与node,pvc等关系 | 需要维护额外的关系信息 |
通过对比发现
- 如果不需要额外数据依赖或者状态维护的部署,优先选择Deployment
- 如果单纯要做数据持久化,方式pod宕机数据丢失,直接使用PV/PVC就可以
- 如果是有多个副本,且每个副本挂载的PV存储数据不同,并且pod宕机重启后仍然关联到之前的PVC,并且数据需要持久化,考虑使用StatefulSet